/*
* Copyright (c) 2016 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.common.truth.extensions.proto;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.extensions.proto.FieldScopeUtil.join;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.truth.extensions.proto.MessageDifferencer.IgnoreCriteria;
import com.google.common.truth.extensions.proto.MessageDifferencer.SpecificField;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Implementations of all variations of {@link FieldScope} logic.
*
* <p>{@code FieldScopeLogic} is the abstract base class which provides common functionality to all
* sub-types. There are two classes of sub-types:
*
* <ul>
* <li>Concrete subtypes, which implements specific rules and perform no delegation.
* <li>Compound subtypes, which combine one or more {@code FieldScopeLogic}s with specific
* operations.
* </ul>
*/
abstract class FieldScopeLogic {
private final Set<Descriptor> validatedDescriptors = Sets.newConcurrentHashSet();
final IgnoreCriteria toIgnoreCriteria(final Descriptor descriptor) {
if (!validatedDescriptors.contains(descriptor)) {
validate(descriptor);
validatedDescriptors.add(descriptor);
}
final Cache cache = new Cache();
return new IgnoreCriteria() {
@Override
public boolean isIgnored(
Message message1,
Message message2,
@Nullable FieldDescriptor fieldDescriptor,
List<SpecificField> fieldPath) {
ImmutableList.Builder<Message> subMessages = ImmutableList.builder();
if (fieldDescriptor != null) {
addSubMessages(fieldDescriptor, message1, subMessages);
addSubMessages(fieldDescriptor, message2, subMessages);
}
Context context = Context.create(descriptor, fieldPath, fieldDescriptor, subMessages);
cache.clearMethodCaches();
return !matchesFieldPath(context, cache) && matchStateAppliesForAllSubPaths(context, cache);
}
};
}
private static void addSubMessages(
FieldDescriptor fieldDescriptor, Message message, ImmutableList.Builder<Message> builder) {
if (fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.MESSAGE) {
return;
}
if (fieldDescriptor.isRepeated()) {
for (int i = 0; i < message.getRepeatedFieldCount(fieldDescriptor); i++) {
builder.add((Message) message.getRepeatedField(fieldDescriptor, i));
}
} else {
builder.add((Message) message.getField(fieldDescriptor));
}
}
// A temporary cache for repeat processing of messages in a single MessageDifferencer run.
// Cache data must be temporary because messages may be mutable, and change between
// MessageDifferencer runs, which invalidates them as keys and invalidates the results.
private static final class Cache {
// Messages do not change between context changes, so this map is scoped to the life of the
// difference operation, which is the life of the Cache object.
private final Map<FieldMatcherLogicBase, Map<Message, Boolean>> messagesWithMatchingField =
Maps.newHashMap();
// These are scoped to the life of the Context object, and must be cleared periodically.
private final Map<FieldScopeLogic, Boolean> matchesFieldPath = Maps.newHashMap();
private final Map<FieldScopeLogic, Boolean> matchStateAppliesForAllSubPaths = Maps.newHashMap();
public Map<Message, Boolean> getMessagesWithMatchingField(FieldMatcherLogicBase key) {
Map<Message, Boolean> map = messagesWithMatchingField.get(key);
if (map == null) {
map = Maps.newHashMap();
messagesWithMatchingField.put(key, map);
}
return map;
}
public boolean matchesFieldPath(FieldScopeLogic logic, Context context) {
@Nullable Boolean match = matchesFieldPath.get(logic);
if (match == null) {
match = logic.doMatchesFieldPath(context, this);
matchesFieldPath.put(logic, match);
}
return match;
}
public boolean matchStateAppliesForAllSubPaths(FieldScopeLogic logic, Context context) {
@Nullable Boolean matchAppliesForAll = matchStateAppliesForAllSubPaths.get(logic);
if (matchAppliesForAll == null) {
matchAppliesForAll = logic.doMatchStateAppliesForAllSubPaths(context, this);
matchStateAppliesForAllSubPaths.put(logic, matchAppliesForAll);
}
return matchAppliesForAll;
}
public void clearMethodCaches() {
matchesFieldPath.clear();
matchStateAppliesForAllSubPaths.clear();
}
}
@AutoValue
abstract static class Context {
/** The Message Descriptor for the message being tested. */
abstract Descriptor descriptor();
/**
* The specific field path leading up to the message containing {@code field()}. If {@code
* field()} is absent, the last element of fieldPath() will describe an unknown field instead.
*/
abstract List<SpecificField> fieldPath();
/**
* The field on the message being inspected, for which we must now determine if it should be
* inspected deeply or not.
*/
abstract Optional<FieldDescriptor> field();
/**
* The message objects at the end of the field path and field descriptor, or empty if there are
* none. MessageDifferencer will omit entire sections of the proto tree if told to ignore root
* messages, so we may need to inspect the contents of the message first to decide if it should
* be ignored.
*/
abstract ImmutableList<Message> messageFields();
static Context create(
Descriptor descriptor,
List<SpecificField> fieldPath,
@Nullable FieldDescriptor field,
ImmutableList.Builder<Message> messageFields) {
return new AutoValue_FieldScopeLogic_Context(
descriptor, fieldPath, Optional.fromNullable(field), messageFields.build());
}
}
/**
* Whether or not this implementation includes the specified specific field path.
*
* <p>Unlike {@link doMatchesFieldPath}, this method does caching, and so is performant to call
* repeatedly. Clients should call this method, but override the do method.
*/
final boolean matchesFieldPath(Context context, Cache cache) {
return cache.matchesFieldPath(this, context);
}
/**
* Whether or not this implementation's answer to {@code matchesFieldPath} is fixed for all sub
* paths of the current specific field path. If fixed, it may be possible to ignore entire
* sub-trees of the protocol buffer for diff inspection.
*
* <p>Returns true by default, since most {@code FieldScopeLogics} include from the root and don't
* exclude subtrees.
*
* <p>Unlike {@link doMatchStateAppliesForAllSubPaths}, this method does caching, and so is
* performant to call repeatedly. Clients should call this method, but override the do method.
*/
final boolean matchStateAppliesForAllSubPaths(Context context, Cache cache) {
return cache.matchStateAppliesForAllSubPaths(this, context);
}
/** Whether or not this implementation includes the specified specific field path. */
abstract boolean doMatchesFieldPath(Context context, Cache cache);
/**
* Whether or not this implementation's answer to {@code matchesFieldPath} is fixed for all sub
* paths of the current specific field path. If fixed, it may be possible to ignore entire
* sub-trees of the protocol buffer for diff inspection.
*
* <p>Returns true by default, since most {@code FieldScopeLogic}s include from the root and don't
* exclude subtrees.
*/
boolean doMatchStateAppliesForAllSubPaths(Context context, Cache cache) {
return true;
}
/**
* Returns an accurate description for debugging purposes.
*
* <p>Compare to {@link FieldScope#usingCorrespondenceString(Optional)}, which returns a beautiful
* error message that makes as much sense to the user as possible.
*
* <p>Abstract so subclasses must implement.
*/
@Override
public abstract String toString();
/**
* Performs any validation that requires a Descriptor to validate against.
*
* @throws IllegalArgumentException if invalid input was provided
*/
void validate(Descriptor descriptor) {}
private static boolean isEmpty(Iterable<?> container) {
boolean isEmpty = true;
for (Object element : container) {
checkNotNull(element);
isEmpty = false;
}
return isEmpty;
}
FieldScopeLogic ignoringFields(Iterable<Integer> fieldNumbers) {
if (isEmpty(fieldNumbers)) {
return this;
}
return and(this, new NegationFieldScopeLogic(new FieldNumbersLogic(fieldNumbers)));
}
FieldScopeLogic ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
if (isEmpty(fieldDescriptors)) {
return this;
}
return and(this, new NegationFieldScopeLogic(new FieldDescriptorsLogic(fieldDescriptors)));
}
FieldScopeLogic allowingFields(Iterable<Integer> fieldNumbers) {
if (isEmpty(fieldNumbers)) {
return this;
}
return or(this, new FieldNumbersLogic(fieldNumbers));
}
FieldScopeLogic allowingFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
if (isEmpty(fieldDescriptors)) {
return this;
}
return or(this, new FieldDescriptorsLogic(fieldDescriptors));
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// CONCRETE SUBTYPES
//////////////////////////////////////////////////////////////////////////////////////////////////
private static final FieldScopeLogic ALL =
new FieldScopeLogic() {
@Override
boolean doMatchesFieldPath(Context context, Cache cache) {
return true;
}
@Override
public String toString() {
return "FieldScopes.all()";
}
};
private static final FieldScopeLogic NONE =
new FieldScopeLogic() {
@Override
boolean doMatchesFieldPath(Context context, Cache cache) {
return false;
}
@Override
public String toString() {
return "FieldScopes.none()";
}
};
static FieldScopeLogic all() {
return ALL;
}
static FieldScopeLogic none() {
return NONE;
}
private static final class PartialScopeLogic extends FieldScopeLogic {
private final Message message;
private final FieldNumberTree fieldNumberTree;
private final Descriptor expectedDescriptor;
PartialScopeLogic(Message message) {
this.message = message;
this.fieldNumberTree = FieldNumberTree.fromMessage(message);
this.expectedDescriptor = message.getDescriptorForType();
}
@Override
void validate(Descriptor descriptor) {
Preconditions.checkArgument(
expectedDescriptor.equals(descriptor),
"Message given to FieldScopes.fromSetFields() does not have the same descriptor as the "
+ "message being tested. Expected %s, got %s.",
expectedDescriptor.getFullName(),
descriptor.getFullName());
}
@Override
boolean doMatchesFieldPath(Context context, Cache cache) {
return fieldNumberTree.matches(context.fieldPath(), context.field());
}
@Override
public String toString() {
return String.format("FieldScopes.fromSetFields(%s)", message);
}
}
static FieldScopeLogic partialScope(Message message) {
return new PartialScopeLogic(message);
}
// TODO(user): Performance: Optimize FieldNumbersLogic and FieldDescriptorsLogic for
// adding / ignoring field numbers and descriptors, respectively, to eliminate high recursion
// costs for long chains of allows/ignores.
// Common functionality for FieldNumbersLogic and FieldDescriptorsLogic.
private abstract static class FieldMatcherLogicBase extends FieldScopeLogic {
/**
* Determines whether the FieldDescriptor is equal to one of the explicitly defined components
* of this FieldScopeLogic.
*
* @param descriptor Descriptor of the message being tested.
* @param fieldDescriptor FieldDescriptor being inspected for a direct match to the scope's
* definition.
*/
abstract boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor);
@Override
final boolean doMatchesFieldPath(Context context, Cache cache) {
if (context.field().isPresent()
&& matchesFieldDescriptor(context.descriptor(), context.field().get())) {
return true;
}
for (SpecificField field : context.fieldPath()) {
FieldDescriptor specificFieldDescriptor = field.getField();
if (specificFieldDescriptor != null
&& matchesFieldDescriptor(context.descriptor(), specificFieldDescriptor)) {
return true;
}
}
return false;
}
@Override
final boolean doMatchStateAppliesForAllSubPaths(Context context, Cache cache) {
// Match is fixed if we match currently, or no sub paths can match.
if (!matchesFieldPath(context, cache)) {
for (Message message : context.messageFields()) {
if (messageHasMatchingField(context, cache, message)) {
return false;
}
}
}
return true;
}
private boolean messageHasMatchingField(Context context, Cache cache, Message message) {
Map<Message, Boolean> messagesWithMatchingField = cache.getMessagesWithMatchingField(this);
if (messagesWithMatchingField.containsKey(message)) {
return messagesWithMatchingField.get(message);
}
boolean result = false;
Map<FieldDescriptor, Object> fields = message.getAllFields();
for (FieldDescriptor key : fields.keySet()) {
if (matchesFieldDescriptor(context.descriptor(), key)) {
result = true;
} else if (key.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
if (key.isRepeated()) {
for (int i = 0; i < message.getRepeatedFieldCount(key); i++) {
if (messageHasMatchingField(
context, cache, (Message) message.getRepeatedField(key, i))) {
result = true;
break;
}
}
} else if (messageHasMatchingField(context, cache, (Message) message.getField(key))) {
result = true;
}
}
if (result) {
break;
}
}
messagesWithMatchingField.put(message, result);
return result;
}
}
// Matches any specific fields which fall under a sub-message field (or root) matching the root
// message type and one of the specified field numbers.
private static final class FieldNumbersLogic extends FieldMatcherLogicBase {
private final ImmutableSet<Integer> fieldNumbers;
FieldNumbersLogic(Iterable<Integer> fieldNumbers) {
this.fieldNumbers = ImmutableSet.copyOf(fieldNumbers);
}
@Override
void validate(Descriptor descriptor) {
super.validate(descriptor);
for (int fieldNumber : fieldNumbers) {
checkArgument(
descriptor.findFieldByNumber(fieldNumber) != null,
"Message type %s has no field with number %s.",
descriptor.getFullName(),
fieldNumber);
}
}
@Override
boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor) {
return fieldDescriptor.getContainingType() == descriptor
&& fieldNumbers.contains(fieldDescriptor.getNumber());
}
@Override
public String toString() {
return String.format("FieldScopes.allowingFields(%s)", join(fieldNumbers));
}
}
// Matches any specific fields which fall under one of the specified FieldDescriptors.
private static final class FieldDescriptorsLogic extends FieldMatcherLogicBase {
private final ImmutableSet<FieldDescriptor> fieldDescriptors;
FieldDescriptorsLogic(Iterable<FieldDescriptor> fieldDescriptors) {
this.fieldDescriptors = ImmutableSet.copyOf(fieldDescriptors);
}
@Override
boolean matchesFieldDescriptor(Descriptor descriptor, FieldDescriptor fieldDescriptor) {
return fieldDescriptors.contains(fieldDescriptor);
}
@Override
public String toString() {
return String.format("FieldScopes.allowingFieldDescriptors(%s)", join(fieldDescriptors));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// COMPOUND SUBTYPES
//////////////////////////////////////////////////////////////////////////////////////////////////
private abstract static class CompoundFieldScopeLogic extends FieldScopeLogic {
final ImmutableList<FieldScopeLogic> elements;
CompoundFieldScopeLogic(FieldScopeLogic singleElem) {
elements = ImmutableList.of(singleElem);
}
CompoundFieldScopeLogic(FieldScopeLogic firstElem, FieldScopeLogic secondElem) {
elements = ImmutableList.of(firstElem, secondElem);
}
@Override
final void validate(Descriptor descriptor) {
for (FieldScopeLogic elem : elements) {
elem.validate(descriptor);
}
}
}
private static final class IntersectionFieldScopeLogic extends CompoundFieldScopeLogic {
IntersectionFieldScopeLogic(FieldScopeLogic subject1, FieldScopeLogic subject2) {
super(subject1, subject2);
}
@Override
boolean doMatchStateAppliesForAllSubPaths(Context context, Cache cache) {
if (matchesFieldPath(context, cache)) {
// We are fixed as true only if both operands are fixed.
return elements.get(0).matchStateAppliesForAllSubPaths(context, cache)
&& elements.get(1).matchStateAppliesForAllSubPaths(context, cache);
} else {
// We are fixed as false only if at least one operand is fixed false.
boolean firstFixedFalse =
!elements.get(0).matchesFieldPath(context, cache)
&& elements.get(0).matchStateAppliesForAllSubPaths(context, cache);
boolean secondFixedFalse =
!elements.get(1).matchesFieldPath(context, cache)
&& elements.get(1).matchStateAppliesForAllSubPaths(context, cache);
return firstFixedFalse || secondFixedFalse;
}
}
@Override
boolean doMatchesFieldPath(Context context, Cache cache) {
return elements.get(0).matchesFieldPath(context, cache)
&& elements.get(1).matchesFieldPath(context, cache);
}
@Override
public String toString() {
return String.format("(%s && %s)", elements.get(0), elements.get(1));
}
}
private static final class UnionFieldScopeLogic extends CompoundFieldScopeLogic {
UnionFieldScopeLogic(FieldScopeLogic subject1, FieldScopeLogic subject2) {
super(subject1, subject2);
}
@Override
boolean doMatchStateAppliesForAllSubPaths(Context context, Cache cache) {
if (matchesFieldPath(context, cache)) {
// We are fixed as true only if either field is fixed true.
boolean firstFixedTrue =
elements.get(0).matchesFieldPath(context, cache)
&& elements.get(0).matchStateAppliesForAllSubPaths(context, cache);
boolean secondFixedTrue =
elements.get(1).matchesFieldPath(context, cache)
&& elements.get(1).matchStateAppliesForAllSubPaths(context, cache);
return firstFixedTrue || secondFixedTrue;
} else {
// We are fixed false only if both operands are fixed false.
return elements.get(0).matchStateAppliesForAllSubPaths(context, cache)
&& elements.get(1).matchStateAppliesForAllSubPaths(context, cache);
}
}
@Override
boolean doMatchesFieldPath(Context context, Cache cache) {
return elements.get(0).matchesFieldPath(context, cache)
|| elements.get(1).matchesFieldPath(context, cache);
}
@Override
public String toString() {
return String.format("(%s || %s)", elements.get(0), elements.get(1));
}
}
private static final class NegationFieldScopeLogic extends CompoundFieldScopeLogic {
NegationFieldScopeLogic(FieldScopeLogic subject) {
super(subject);
}
@Override
boolean doMatchStateAppliesForAllSubPaths(Context context, Cache cache) {
// We are fixed only if the operand is fixed.
return elements.get(0).matchStateAppliesForAllSubPaths(context, cache);
}
@Override
boolean doMatchesFieldPath(Context context, Cache cache) {
return !elements.get(0).matchesFieldPath(context, cache);
}
@Override
public String toString() {
return String.format("!(%s)", elements.get(0));
}
}
static FieldScopeLogic and(FieldScopeLogic fieldScopeLogic1, FieldScopeLogic fieldScopeLogic2) {
return new IntersectionFieldScopeLogic(fieldScopeLogic1, fieldScopeLogic2);
}
static FieldScopeLogic or(FieldScopeLogic fieldScopeLogic1, FieldScopeLogic fieldScopeLogic2) {
return new UnionFieldScopeLogic(fieldScopeLogic1, fieldScopeLogic2);
}
static FieldScopeLogic not(FieldScopeLogic fieldScopeLogic) {
return new NegationFieldScopeLogic(fieldScopeLogic);
}
}